/**
 * Copyright 2007 Cordys R&D B.V. 
 * 
 * This file is part of the UIUnit framework. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
 package com.cordys.cm.uiunit.framework.internal.selenium;

import static java.util.regex.Pattern.compile;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import org.apache.log4j.Logger;
import org.junit.Ignore;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

import sun.misc.BASE64Decoder;

import com.cordys.cm.uiunit.exceptions.UIUnitRuntimeException;
import com.cordys.cm.uiunit.framework.TestFrameworkInformation;
import com.cordys.cm.uiunit.framework.selenium.ISeleniumEnvironment;
import com.cordys.cm.uiunit.utils.Screendumper;
import com.cordys.tools.pathfinder.JMXServer.JMXMBean;
import com.eibus.xml.nom.Document;
import com.eibus.xml.nom.Node;
import com.eibus.xml.nom.XMLException;
import com.thoughtworks.selenium.SeleniumException;

public class SeleniumRunListener extends RunListener{

	private ISeleniumEnvironment seleniumEnv;
	
	private static final Document doc = new Document();
	private static final String TIMEOUT_TEXT_LAYOUT = "Timeout: %s";
	
	private static final String TIMELEFT_TEXT_LAYOUT = "Timeout settings: %s  Time left: %s";
	
	private static final String CLASSREGEX = "^(\\w+?\\()?(\\w+?(\\.\\w+?)+?)[\\)]?$";
	
	private static final Pattern CLASSNAME_PATTERN = compile(CLASSREGEX);
	
	private static final String METHODREGEX = "^(\\w+?)\\(\\w+(\\.\\w+)+?\\)$";
	
	private static final Pattern METHODNAME_PATTERN = Pattern.compile(METHODREGEX);

	private int totalTests = 0;
	
	private long timeoutTime = 0;
	
	private long startTime = 0;

	private int runnedTests = 0;

	private int totalFailures = 0;

	private int totalIgnored = 0;

	private int totalErrors = 0;

	private Logger logger;

	//private int rootNode;

	JMXServiceURL url;

	JMXConnector jmxc;

	static MBeanServerConnection mbsc;

	ObjectName mbeanName;

	public boolean hasacknowledged = false;

	static JMXMBean mbeanProxy;
	
	NotificationBroadcasterSupport nbSupport;

	HashMap<String, Integer> hashMap = new HashMap<String, Integer>();

	ArrayList<Integer> testcaselist = new ArrayList<Integer>();

	public SeleniumRunListener(ISeleniumEnvironment seleniumEnv) 
	{
		this.seleniumEnv = seleniumEnv;
		this.logger = Logger.getLogger(TestFrameworkInformation.class.getName());
	}
	
	public void setTotalNumberOfTest(int total)
	{
		totalTests = total;
	}
	
	public void startTimeout(long time)
	{
		this.startTime = System.currentTimeMillis();
		if (time == -1)
		{
			this.timeoutTime = -1;
		}
		else
		{
			this.timeoutTime = time;
			logger.debug(String.format(TIMEOUT_TEXT_LAYOUT, formatDuration(this.timeoutTime)));
		}
	}

	public void stopTimeout()
	{
		if(this.timeoutTime >0)
		{
			long left = (startTime + timeoutTime) - System.currentTimeMillis();
			logger.debug(String.format(TIMELEFT_TEXT_LAYOUT, formatDuration(timeoutTime), formatDuration(left)));
		}
		this.timeoutTime = 0;
	}
	
	private static String formatDuration(long durationInMs)
	{
		long milliSecondsPerHour = 60 * 60 * 1000;
		long hours = durationInMs / milliSecondsPerHour;
		long minutes = (durationInMs / 60 / 1000) % 60;
		long seconds = (durationInMs / 1000) % 60;
		long miliseconds = durationInMs % 1000;

		return String.format("%02d:%02d:%02d.%03d ",
												 hours,
												 minutes,
												 seconds,
												 miliseconds);
	}
	
	@Override
	public void testFailure(Failure failure)
	{
		if (failure.getException() instanceof AssertionError)
		{
			totalFailures++;
		}
		else
		{
			totalErrors++;
		}

		logger.error(String.format( "Test FAILED: %03d/%03d - %s", runnedTests, totalTests , failure.getDescription()), failure.getException() );
		dumpScreen(failure);
	}

	@Override
	public void testFinished(Description description) throws Exception 
	{
		if(this.seleniumEnv.getConfiguration().getFEPathFinder().equals("true")){
			getSourceFile(description);
		}
		this.log("Test finished: %03d/%03d - %s", runnedTests, totalTests,
				description.getDisplayName());
	}

	@Override
	public void testIgnored(Description description)
	{
		totalIgnored++;
		runnedTests++;
		this.log("Test IGNORED:  %03d/%03d - %2s", runnedTests, totalTests , description.getDisplayName());
		logIgnoreReason(description);
	}

	private void logIgnoreReason(Description description)
	{
		String testClassName = getTestClass(description);
		String testName = getTestName(description);
		try
		{
			Ignore ignore;
			Class<?> testClass = Class.forName(testClassName); // get class instance
			if (testName.length() > 0) // if a testname is specified (ignore on test method)
			{
				// get the method
				Method method = testClass.getMethod(testName);
				// get @Ignore from the method
				ignore = method.getAnnotation(Ignore.class);
			}
			else // if no testname is specified (ignore on class)
			{
				// get @Ignore from the class
				ignore = testClass.getAnnotation(Ignore.class);
			}
			//Due to prerequisite testing, it is possible to ignore without Ignore annotation
			if(null != ignore)
			{
    		String ignoreReason = ignore.value();
    		
    		if("".equals(ignoreReason))
    		{
    			ignoreReason = "No reason provided, please use '@Ignore(\"Reason why to ignore the test\")'";
    		}
    		this.log("IGNORE Reason: %s", ignoreReason);
			}
		}
		catch (ClassNotFoundException e)
		{
			throw new UIUnitRuntimeException(e);
		}
		catch (SecurityException e)
		{
			throw new UIUnitRuntimeException(e);
		}
		catch (NoSuchMethodException e)
		{
			throw new UIUnitRuntimeException(e);
		}
	}

	@Override
	public void testRunFinished(Result result)
	{
		if(this.seleniumEnv.getConfiguration().getFEPathFinder().equals("true")){
			writeToFile();
		}
		this.log("Testrun finished");
		this.log("Tests run: %s, Failures: %s, Errors: %s, Ignored: %s, Time elapsed: %s",
						 result.getRunCount(),
						 totalFailures,
						 totalErrors,
						 result.getIgnoreCount(),
						 result.getRunTime());
		stopFramework();
	}

	@Override
	public void testRunStarted(Description description)
	{
		this.log("Testrun started: %s", description.getDisplayName());
	}

	@Override
	public void testStarted(Description description)
	{
	    if(this.seleniumEnv.getConfiguration().getBEPathFinder().equals("true"))
	    	connectJMXServer(getTestName(description));
		if(this.seleniumEnv.getConfiguration().getFEPathFinder().equals("true"))
			this.seleniumEnv.getSeleniumObject().getEval("CordysRoot.callstack = new Array()");

		runnedTests++;
		this.log("Test started:  %03d/%03d - %s", runnedTests, totalTests, description.getDisplayName());
	}
	
	private String getTestName(Description description)
	{
		Matcher matcher = METHODNAME_PATTERN.matcher(description.getDisplayName());
		if (matcher.find())
		{
			return matcher.group(1);
		}
		return "";
	}
	
	private String getTestClass(Description description)
	{
		Matcher matcher = CLASSNAME_PATTERN.matcher(description.getDisplayName());
		if(matcher.find())
		{
			return matcher.group(2);
		}
		return "";
	}
	
	public void checkStopFramework()
	{
		if(runnedTests == totalTests)
		{
			this.log(String.format("Total runned tests: %03d. End of suite is reached, stopping framework", runnedTests));
			stopFramework();
		}
	}
	
	private void stopFramework()
	{
		//close the browser window
		this.log("Browser will stop");
		try
		{
		this.seleniumEnv.getSeleniumObject().close();
		this.seleniumEnv.getSeleniumObject().stop();
		}
		catch(SeleniumException e)
		{
			//Ignore
		}
	}

	private void log(String message, Object... parameters)
	{
		logger.info(String.format(message, parameters));
	}
	
	private void dumpScreen(Failure failure)
	{
		try {
			Screendumper dumper = new Screendumper();
			String filename = String.format("%03d - %s", runnedTests, failure
					.getDescription().getDisplayName());

			String screenshotString = seleniumEnv.getSeleniumObject()
			.captureScreenshotToString();
			BufferedImage image = null;
			try {
				BASE64Decoder decoder = new BASE64Decoder();
				byte[] imageBytes = decoder.decodeBuffer(screenshotString);
				image = ImageIO.read(new ByteArrayInputStream(imageBytes));
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(
						"Error during creation of screendump: "
						+ e.getMessage(), e);
			} catch (IOException e) {
				throw new RuntimeException(
						"Error during creation of screendump: "
						+ e.getMessage(), e);
			}

			dumper.savePageToPngFile(image, filename);
		} catch (Throwable t) {
			t.printStackTrace();
			throw new RuntimeException(t);
		}
	}

	private void getSourceFile(Description description) throws UnsupportedEncodingException, XMLException 
	{
		String testClassName = getTestClass(description);
		String testName = getTestName(description);
		String output=this.seleniumEnv.getSeleniumObject().getEval("CordysRoot.callstack[0]");
		String result = this.seleniumEnv.getSeleniumObject().getEval("CordysRoot.callstack.length");
		int methodCount = (result != null && !result.equals("")) ? Integer.parseInt(result) : 0;
	    int methodNode = doc.parseString("<method methodname=\"\" filename=\"\"/>");
	    int start=1; 
	    int testClassRootNode =0;
	    // getting upto 500 entries from the callstack array and appending it to the string
	    int end=500;

	    while(end<=methodCount)
		{
	    	output= output+","+this.seleniumEnv.getSeleniumObject().getEval("CordysRoot.callstack.slice("+start+","+end+")");
		  	start=end+1;
		  	end=end+500;
	    }
	    if(methodCount<500 || methodCount%500!=0)
	    {
		    output= output+","+this.seleniumEnv.getSeleniumObject().getEval("CordysRoot.callstack.slice("+start+","+methodCount+")");
	    }
	       
	    /**
	     * As TestSuit contains meny classes it needs to create Xmlfile for each class
	     * In that case we need to check whether that class already exists.
	     *
	     */
	    if (!hashMap.containsKey(testClassName))
	    {
	    	testClassRootNode = doc.parseString("<root/>");
		} 
	    else 
		{
	    	testClassRootNode = hashMap.get(testClassName);
		}
	    
	    //creating testcase node with current TestClassName and TestCaseName
		int testcaseNode = Node.createElement("testcase", testClassRootNode);
		Node.setAttribute(testcaseNode, "name", testName);
		Node.setAttribute(testcaseNode, "class", testClassName);
	    
	    //appending all the Source file details to one root node
	    String[] methods = output.split("\\,");
	    
	    // Now storing the information to the testcase node under the corresponding testclass rootnode got from the hashmap
	   	for(String methodName : methods)
		{
			int method = Node.clone(methodNode, true);
			String file[]=methodName.split("\\*");
			Node.setAttribute(method, "methodname", file[1]);
			Node.setAttribute(method, "filename", file[0]);
			Node.appendToChildren(method, testcaseNode);
	    }
	   	
		Node.appendToChildren(testcaseNode, testClassRootNode);
		hashMap.put(testClassName, testClassRootNode);
		this.log("Test finished: %03d/%03d - %s", runnedTests, totalTests,description.getDisplayName());
	}

	private void writeToFile() 
	{
		File pathFinderFolder = new File(System.getenv("PATHFINDER_HOME")+"\\output");
		if (!pathFinderFolder.exists())
			pathFinderFolder.mkdir();
		Iterator<String> testClassIterator = hashMap.keySet().iterator();
		int testClassRootNode=0;
		while (testClassIterator.hasNext())
		{
				String className = testClassIterator.next();
				testClassRootNode = hashMap.get(className) ;
				String fileName =pathFinderFolder+"\\"+className +".xml";
				try {
					Node.writeToFile(testClassRootNode, testClassRootNode, fileName, 1);
					Node.delete(testClassRootNode);
				} catch (XMLException e) {
					e.printStackTrace();
				}
		}
	}

	private void connectJMXServer(String testName) 
	{
		final Properties properties = new Properties();
		try
		{
			properties.load(new FileInputStream(System.getenv("PATHFINDER_HOME") + "\\aspect.properties"));
			
			url = new JMXServiceURL(properties.getProperty("JMX"));
			System.out.println("[JMXClient Test Side] " + properties.getProperty("JMX"));
			
			jmxc = JMXConnectorFactory.connect(url, null);
			mbsc = jmxc.getMBeanServerConnection();
			mbeanName = new ObjectName(properties.getProperty("mbean"));
			mbeanProxy = JMX.newMBeanProxy(mbsc, mbeanName, JMXMBean.class,true);
			mbeanProxy.setTestCaseName(testName);
		} 
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		} 
		catch (IOException e) 
		{
			e.printStackTrace();
		} 
		catch (MalformedObjectNameException e) 
		{
			e.printStackTrace();
		} 
		catch (NullPointerException e)
		{
			e.printStackTrace();
		} 

	}	
}
